﻿#region License
/*  YanYeovil
    Copyright (c) 2014, Alexey A. Popov
    All rights reserved.

    Redistribution and use in source and binary forms, with or without modification, are
    permitted provided that the following conditions are met:

    - Redistributions of source code must retain the above copyright notice, this list
      of conditions and the following disclaimer.

    - Redistributions in binary form must reproduce the above copyright notice, this list
      of conditions and the following disclaimer in the documentation and/or other materials
      provided with the distribution.

    - Neither the propertyName of the Alexey A. Popov nor the names of its contributors may be used to
      endorse or promote products derived from this software without specific prior written
      permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS *AS IS* AND ANY EXPRESS
    OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
    AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
    CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
    IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
    OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;

namespace Pvax.Data
{
    /// <summary>
    /// Maps a type to a DB table.
    /// </summary>
    class TypeMapping
    {
        //// This string array is loaded form resource
        static string[] reservedWords;

        //// This static constructor loads the list of reserved words from a resource with name 'reserved.txt'.
        //// The list of words must be sorted by the Array.Sort routine for binary search to work correctly!
        //// Current list is taken from: https://www.drupal.org/node/141051
        //// Be cautios when editing the list.
        static TypeMapping()
        {
            List<string> words = new List<string>();
            string line;
            using(TextReader reader = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(DbConnectionExtender), "reserved.txt")))
            {
                while(null != (line = reader.ReadLine()))
                    words.Add(line);
            }
            reservedWords = words.ToArray();
        }

        static string ColumnName(string propertyName)
        {
            if(Array.BinarySearch(reservedWords, propertyName.ToUpper()) > 0)
                return "[" + propertyName + "]";
            else
                return propertyName;
        }

        Type type;

        Dictionary<string, PropertyMapping> properties;

        static object GetAttribute(MemberInfo memberInfo, Type type)
        {
            Debug.Assert(typeof(Attribute).IsAssignableFrom(type));
            object[] attrs = memberInfo.GetCustomAttributes(type, true);
            if(attrs.Length > 0)
                return attrs[0];
            else
                return null;
        }

        static bool IsMarkedByAttribute(MemberInfo memberInfo, Type type)
        {
         return null != GetAttribute(memberInfo, type);
        }

        static bool IsPrimaryKey(PropertyInfo propInfo)
        {
            return IsMarkedByAttribute(propInfo, typeof(YyPrimaryKeyAttribute));
        }

        // TODO
        static bool IsForeignKey(PropertyInfo propInfo)
        {
            return IsMarkedByAttribute(propInfo, typeof(YyForeignKeyAttribute));
        }

        static bool ShouldIgnore(PropertyInfo propInfo)
        {
            return IsMarkedByAttribute(propInfo, typeof(YyIgnoreAttribute));
        }

        /// <summary>
        /// Creates and initializes a new instance of the <see cref="TypeMapping"/> class.
        /// </summary>
        /// <param name="type">The type to map.</param>
        internal TypeMapping(Type type)
        {
            this.type = type;
            this.properties = new Dictionary<string, PropertyMapping>();
            PropertyInfo[] properties = type.GetProperties();
            foreach(var pi in type.GetProperties())
            {
                if(pi.CanRead && pi.CanWrite && !ShouldIgnore(pi))
                {
                    PropertyMapping pm = new PropertyMapping(pi);
                    if(null == primaryKey)
                    {
                        if(IsPrimaryKey(pi) || pi.Name.ToLowerInvariant() == "id")
                            primaryKey = pm;
                    }
                    this.properties.Add(pi.Name, pm);
                }
            }
            if(null == primaryKey)
                throw new ObjectMappingException(String.Format(Pvax.Data.Properties.Resources.ErrNoPK, type.FullName));
        }

        /// <summary>
        /// Gets the type this instance maps.
        /// </summary>
        /// <value>
        /// The type.
        /// </value>
        internal Type Type
        {
            get
            {
                return this.type;
            }
        }

        /// <summary>
        /// Gives access to the property mappings for the type.
        /// </summary>
        /// <value>
        /// An instance of the <see cref="IDictionary{TKey, TValue}"/> that exposes
        /// all the mapped properties of the type.
        /// </value>
        internal IDictionary<string, PropertyMapping> Properties
        {
            get
            {
                return properties;
            }
        }

        PropertyMapping primaryKey;

        /// <summary>
        /// Gets the field mapping for the primary key.
        /// </summary>
        /// <value>
        /// An instance of the <see cref="PropertyMapping"/> that represents the primary key
        /// in the database.
        /// </value>
        internal PropertyMapping PrimaryKey
        {
            get
            {
                return primaryKey;
            }
        }

        string selectAllText;

        string GetSelectAllText(Func<string, string> tableNameMapper)
        {
            if(null == selectAllText)
            {
                StringBuilder commandText = new StringBuilder("SELECT ");
                foreach(var kv in properties)
                {
                    commandText.Append(ColumnName(kv.Value.Name));
                    commandText.Append(',');
                }
                commandText.Length -= 1;
                commandText.Append(" FROM ");
                commandText.Append(tableNameMapper(type.Name));
                selectAllText = commandText.ToString();
            }
            return selectAllText;
        }

        /// <summary>
        /// Creates a query that selects all the records from the database.
        /// </summary>
        /// <param name="connection">The database connection.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>An instance of the <see cref="IDbCommand"/> that executes the query.</returns>
        internal IDbCommand CreateSelectAllCommand(IDbConnection connection, Func<string, string> tableNameMapper)
        {
            IDbCommand result = connection.CreateCommand();
            result.CommandText = GetSelectAllText(tableNameMapper);
            return result;
        }

        string selectText;

        string GetSelectText(Func<string, string> tableNameMapper)
        {
            if(null == selectText)
            {
                StringBuilder commandText = new StringBuilder("SELECT ");
                foreach(var kv in properties)
                {
                    commandText.Append(ColumnName(kv.Value.Name));
                    commandText.Append(',');
                }
                commandText.Length -= 1;
                commandText.Append(" FROM ");
                commandText.Append(tableNameMapper(type.Name));
                commandText.Append(" WHERE ");
                foreach(var kv in properties)
                {
                    if(kv.Value == PrimaryKey)
                        {
                        commandText.Append(ColumnName(kv.Value.Name));
                        commandText.Append("=?");
                        break;
                    }
                }
                selectText = commandText.ToString();
            }
            return selectText;
        }

        /// <summary>
        /// Creates a parameterized query that selects a record from the databse by the primary key.
        /// </summary>
        /// <param name="connection">The database connection.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>An instance of the <see cref="IDbCommand"/> that executes the query.</returns>
        internal IDbCommand CreateSelectCommand(IDbConnection connection, Func<string, string> tableNameMapper)
        {
            IDbCommand result = connection.CreateCommand();
            result.CommandText = GetSelectText(tableNameMapper);
            foreach(var kv in properties)
            {
                if(PrimaryKey == kv.Value)
                {
                    IDataParameter parameter = result.CreateParameter();
                    parameter.DbType = kv.Value.DbType;
                    result.Parameters.Add(parameter);
                    break;
                }
            }
            return result;
        }

        string selectByText;

        string GetSelectByText(PropertyMapping field, Func<string, string> tableNameMapper)
        {
            if(null == selectByText)
            {
                StringBuilder commandText = new StringBuilder("SELECT ");
                foreach(var kv in properties)
                {
                    commandText.Append(ColumnName(kv.Value.Name));
                    commandText.Append(',');
                }
                commandText.Length -= 1;
                commandText.Append(" FROM ");
                commandText.Append(tableNameMapper(type.Name));
                commandText.Append(" WHERE ");
                commandText.Append(field.Name);
                commandText.Append("=?");
                selectByText = commandText.ToString();
            }
            return selectByText;
        }

        internal IDbCommand CreateSelectByCommand(IDbConnection connection, PropertyMapping field, Func<string, string> tableNameMapper)
        {
            IDbCommand result = connection.CreateCommand();
            result.CommandText = GetSelectByText(field, tableNameMapper);
            IDataParameter parameter = result.CreateParameter();
            parameter.DbType = field.DbType;
            result.Parameters.Add(parameter);
            return result;
        }

        string insertText;

        string GetInsertText(Func<string, string> tableNameMapper)
        {
            if(null == insertText)
            {
                StringBuilder commandText = new StringBuilder("INSERT INTO ");
                commandText.Append(tableNameMapper(type.Name));
                commandText.Append(" (");
                foreach(var kv in properties)
                {
                    commandText.Append(ColumnName(kv.Value.Name));
                    commandText.Append(',');
                }
                commandText.Length -= 1;
                commandText.Append(") VALUES (");
                foreach(var kv in properties)
                {
                    commandText.Append("?,");
                }
                commandText.Length -= 1;
                commandText.Append(")");
                insertText = commandText.ToString();
            }
            return insertText;
        }

        /// <summary>
        /// Creates a parameterized query that inserts a new record to the databse.
        /// </summary>
        /// <param name="connection">The database connection.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>An instance of the <see cref="IDbCommand"/> that executes the query.</returns>
        internal IDbCommand CreateInsertCommand(IDbConnection connection, Func<string, string> tableNameMapper)
        {
            IDbCommand result = connection.CreateCommand();
            result.CommandText = GetInsertText(tableNameMapper);
            foreach(var kv in properties)
            {
                IDataParameter parameter = result.CreateParameter();
                parameter.DbType = kv.Value.DbType;
                result.Parameters.Add(parameter);
            }
            return result;
        }

        string deleteText;

        string GetDeleteText(Func<string, string> tableNameMapper)
        {
            if(null == deleteText)
            {
                StringBuilder commandText = new StringBuilder("DELETE FROM ");
                commandText.Append(tableNameMapper(type.Name));
                commandText.Append(" WHERE ");
                foreach(var kv in properties)
                {
                    if(kv.Value == PrimaryKey)
                    {
                        commandText.Append(ColumnName(kv.Value.Name));
                        commandText.Append("=?");
                        break;
                    }
                }
                deleteText = commandText.ToString();
            }
            return deleteText;
        }

        /// <summary>
        /// Creates a parameterized query that deletes a record from the databse.
        /// </summary>
        /// <param name="connection">The database connection.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>An instance of the <see cref="IDbCommand"/> that executes the query.</returns>
        internal IDbCommand CreateDeleteCommand(IDbConnection connection, Func<string, string> tableNameMapper)
        {
            IDbCommand result = connection.CreateCommand();
            result.CommandText = GetDeleteText(tableNameMapper);
            foreach(var kv in properties)
            {
                if(kv.Value == PrimaryKey)
                {
                    IDataParameter parameter = result.CreateParameter();
                    parameter.DbType = kv.Value.DbType;
                    result.Parameters.Add(parameter);
                    break;
                }
            }
            return result;
        }

        string deleteByText;

        string GetDeleteByText(PropertyMapping field, Func<string, string> tableNameMapper)
        {
            if(null == deleteByText)
            {
                StringBuilder commandText = new StringBuilder("DELETE FROM ");
                commandText.Append(tableNameMapper(type.Name));
                commandText.Append(" WHERE ");
                commandText.Length -= 1;
                commandText.Append(" FROM ");
                commandText.Append(tableNameMapper(type.Name));
                commandText.Append(" WHERE ");
                commandText.Append(field.Name);
                commandText.Append("=?");
                deleteByText = commandText.ToString();
            }
            return deleteByText;
        }

        internal IDbCommand CreateDeleteByCommand(IDbConnection connection, PropertyMapping field, Func<string, string> tableNameMapper)
        {
            IDbCommand result = connection.CreateCommand();
            result.CommandText = GetDeleteByText(field, tableNameMapper);
            IDataParameter parameter = result.CreateParameter();
            parameter.DbType = field.DbType;
            result.Parameters.Add(parameter);
            return result;
        }

        string updateText;

        string GetUpdateText(Func<string, string> tableNameMapper)
        {
            if(null == updateText)
            {
                StringBuilder commandText = new StringBuilder("UPDATE ");
                commandText.Append(tableNameMapper(type.Name));
                commandText.Append(" SET ");
                foreach(var kv in properties)
                {
                    if(kv.Value != PrimaryKey)
                    {
                        commandText.Append(ColumnName(kv.Value.Name));
                        commandText.Append("=?,");
                    }
                }
                commandText.Length -= 1;
                commandText.Append(" WHERE ");
                foreach(var kv in properties)
                {
                    if(kv.Value == PrimaryKey)
                    {
                        commandText.Append(ColumnName(kv.Value.Name));
                        commandText.Append("=?");
                        break;
                    }
                }
                updateText = commandText.ToString();
            }
            return updateText;
        }

        /// <summary>
        /// Creates a parameterized query that updates a record in the databse.
        /// </summary>
        /// <param name="connection">The database connection.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>An instance of the <see cref="IDbCommand"/> that executes the query.</returns>
        internal IDbCommand CreateUpdateCommand(IDbConnection connection, Func<string, string> tableNameMapper)
        {
            IDbCommand result = connection.CreateCommand();
            result.CommandText = GetUpdateText(tableNameMapper);
            //// Skip primary key
            foreach(var kv in properties)
            {
                if(PrimaryKey == kv.Value)
                    continue;
                IDataParameter parameter = result.CreateParameter();
                parameter.DbType = kv.Value.DbType;
                result.Parameters.Add(parameter);
            }
            //// Make the primary key parameter the last one
            foreach(var kv in properties)
            {
                if(PrimaryKey == kv.Value)
                {
                    IDataParameter parameter = result.CreateParameter();
                    parameter.DbType = kv.Value.DbType;
                    result.Parameters.Add(parameter);
                    break;
                }
            }
            return result;
        }
    }
}
